<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage grove
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;


interface ProfileMapper {

    /**
     * Get the profile class name for the given profile name
     *
     * @param string $profileName
     * @return string
     */
    public function getProfileClass($profileName);

    /**
     * Get all profiles in an equal or parenting namespace for the class of given name
     *
     * @param string $className
     * @return array
     */
    public function getEnclosingProfiles($className);

    /**
     * Get all profiles in an equal or lower located namespace for the class of given name
     *
     * @param string $className
     * @return array
     */
    public function getEnclosedProfiles($className);

    /**
     * Get all named profiles
     *
     * @return array
     */
    public function getProfiles();
}

class XmlBasedProfileMapper implements ProfileMapper {

    /**
     * @var Storage
     */
    private $mappings = null;

    /**
     * @var Storage
     */
    private $properties = null;


    public function __construct($projectPath) {

        $storageManager = get('mg\StorageManager');
        $runtime = get('mg\Runtime');
        
        $mappings = $storageManager->getStorage('xmlbasedprofilemapper.mappings', true);
        $this->mappings = $storageManager->cache($mappings);

        $properties = $storageManager->getStorage('xmlbasedprofilemapper.properties', true);
        $this->properties = $storageManager->cache($properties);

        if($runtime->isTransactional()) {
            $this->rebuild($projectPath);
        }
    }

    private function rebuild($projectPath) {
        $properties = $this->properties;
        $mappings = $this->mappings;

        $profiles = isset($properties['profiles']) ? unserialize($properties['profiles']) : array();

        /* @var $architecture \mg\Architecture */

        $architecture = get('mg\Architecture');
        
        $realProjectPath = realpath($projectPath);
        
        if($realProjectPath === false) {
            throw new \mg\Exception("Project path '{$projectPath}' is not accessible");
        }
        
        $mappings->clear();

        $iterator = new \RecursiveIteratorIterator(
            new RegexRecursiveDirectoryFilterIterator(
            new \RecursiveDirectoryIterator($realProjectPath, \RecursiveDirectoryIterator :: CURRENT_AS_FILEINFO)
                ,'#^[^.].*$#i'
                ,'#^[^.].*\.xml#i'));

        $files = array();

        /* Gather all files */
        foreach($iterator as $filename => $file) {
            if(!$file->isReadable()) {
                continue;
            }

            /* @var $xml \SimpleXmlElement */
            $element = simplexml_load_file($filename);
            $element->registerXpathNamespace('mg', 'http://code.google.com/p/mgframework/grove');

            $files[$filename][0] = $element->xpath('/mg:project/mg:profiles/mg:profile');
            $files[$filename][1] = $element->xpath('/mg:project/mg:profiles/mg:compositeProfile');
        }

        $registeredProfiles = array();
        
        /* Examine profiles first */
        foreach($files as $filename => $data) {
            list($profileElements, $_) = $data;

            foreach($profileElements as $profile) {
                $name = mb_strtolower(trim((string)$profile->attributes()->name));
                $class = trim((string)$profile->attributes()->class);
                $class = ltrim($class, '\\');

                if(empty($name) && !empty($class)) {
                    throw new Exception("No name set for profile with class '{$class}' in '{$filename}'");
                }

                if(empty($class) && !empty($name)) {
                    throw new Exception("No class set for profile with name '{$name}' in '{$filename}'");
                }

                if(empty($class) && empty($name)) {
                    throw new Exception("Empty profile in '{$filename}'");
                }
                
                if($architecture->getClass($class) === null) {
                    throw new Exception("Class '{$class}' for profile '{$name}' in '{$filename}' does not exist");
                }

                if($architecture->getClassDistance($class, 'mg\Profile') === false) {
                    throw new Exception("Class '{$class}' for profile '{$name}' in '{$filename}' is not of type 'mg\Profile'");
                }
                
                if(isset($mappings[$name])) {
                    throw new Exception("Duplicate profile '{$name}' in '{$filename}'");
                }

                $mappings[$name] = mb_strtolower($class); 
                $profiles[$name] = mb_strtolower($class);
                $registeredProfiles[$name] = true;
            }
        }
        
        /* Resolve composite profile dependencies */
        while(count($files) > 0) {

            $progress = false;

            $noComposites = true;
            /* referenced in order to manipulate while iterating */
            foreach($files as $filename => &$data) {
                
                $compositeProfileElements =& $data[1];
                
                /* analyze every element for its dependencies */
                foreach($compositeProfileElements as $index => $compositeProfile) {
                    $maybeProgress = false;
                    $name = mb_strtolower(trim((string)$compositeProfile->attributes()->name));
    
                    if(isset($profiles[$name]) || isset($compositeProfiles[$name])) {
                        throw new Exception("Duplicate profile '{$name}' in '{$filename}'");
                    }
    
                    $referencedProfiles = $compositeProfile->xpath('./mg:profile');

                    if(count($referencedProfiles) === 0) {
                        throw new Exception("Empty composite profile '{$name}' in '{$filename}'");
                    }

                    $allRegistered = true;

                    foreach($referencedProfiles as $referencedProfile) {
                        $referencedName = mb_strtolower(trim((string)$referencedProfile->attributes()->name));
                        $allRegistered = $allRegistered && isset($registeredProfiles[$referencedName]);
                    }

                    if($allRegistered) {
                        $registeredProfiles[$name] = true;
                        $progress = true;
                        unset($compositeProfileElements[$index]);
                    }
                }
                
                /* If no more composite profiles, unset file */
                if(count($compositeProfileElements) === 0) {
                    unset($files[$filename]);
                }
                
                /* Prevent over-referencing */
                unset($compositeProfileElements);
                unset($data);
            }

            $progress = $progress || $noComposites;
            /*
             * Nothing was done, analyze current dependencies and
             * throw an error to indicate these could not be resolved.
             */
            if(!$progress) {
                $message = array();

                /* Create big error message */
                foreach($files as $filename => $data) {
                    $compositeProfileElements = $data[1];
                    $names = array();

                    foreach($compositeProfileElements as $compositeProfile) {
                        $name = mb_strtolower(trim((string)$compositeProfile->attributes()->name));
                        $names[] = "'$name'";
                    }
                    $message[] = '{' . implode(',', $names) . " in '{$filename}'}";
                }
                $message = implode(',', $message);

                throw new Exception("Composite profiles {$message} could not be resolved");
            }
  
        }

        $properties['profiles'] = serialize($profiles);
    }
    
    /**
     * (non-PHPdoc)
     * @see framework/mg.ProfileMapper::getProfileClass($profileName)
     */
    public function getProfileClass($profileName) {
        $profileName = mb_strtolower($profileName);

        if(isset($this->mappings[$profileName])) {
            return $this->mappings[$profileName];
        }

        return null;
    }

    /**
     * (non-PHPdoc)
     * @see framework/mg.ProfileMapper::getEnclosingProfiles($className)
     */
    public function getEnclosingProfiles($className) {
        return array();
    }

    /**
     * (non-PHPdoc)
     * @see framework/mg.ProfileMapper::getEnclosedProfiles($className)
     */
    public function getEnclosedProfiles($className) {
        return array();
    }

    /**
     * (non-PHPdoc)
     * @see framework/mg.ProfileMapper::getProfiles()
     */
    public function getProfiles() {
        $profiles = isset($this->properties['profiles']) ? unserialize($this->properties['profiles']) : array();

        return array_values($profiles);
    }
}

class NamespaceBasedProfileMapper implements ProfileMapper {

    /**
     * @var Storage
     */
    private $mappings = null;

    /**
     * @var Storage
     */
    private $properties = null;

    public function __construct(StorageManager $storageManager, Runtime $runtime) {

        $mappings = $storageManager->getStorage('namespacebasedprofilemapper.mappings', true);
        $this->mappings = $storageManager->cache($mappings);

        $properties = $storageManager->getStorage('namespacebasedprofilemapper.properties', true);
        $this->properties = $storageManager->cache($properties);

        if($runtime->isTransactional()) {
            $this->rebuild();
        }
    }

    private function rebuild() {
        $mappings = $this->mappings;
        $properties = $this->properties;

        in(function(Architecture $architecture) use($mappings, $properties) {
            // find all profiles
            // make sure them all be unique (one profile per namespace)

            $profiles = isset($properties['profiles']) ? unserialize($properties['profiles']) : array();
            	
            $classUpdates = $architecture->getUpdates();

            $profileClasses = array();

            /* @var $classUpdate ClassUpdate */
            foreach($classUpdates as $classUpdate) {
                $className = mb_strtolower($classUpdate->getQualifiedName());

                $profileName = mb_strtolower($classUpdate->getNamespace()) . '\\';

                if(isset($profiles[$profileName]) && $profiles[$profileName] === $className) {
                    unset($profiles[$profileName]);
                    unset($mappings[$profileName]);
                }
            }

            foreach($classUpdates as $classUpdate) {
                if($classUpdate->isRemoved()) {
                    continue;
                }

                $className = mb_strtolower($classUpdate->getQualifiedName());

                if(!($architecture->getClassDistance($className, 'mg\Profile') > 0)) {
                    continue;
                }

                $profileName = mb_strtolower($classUpdate->getNamespace()) . '\\';

                if(isset($profiles[$profileName])) {
                    throw new Exception("Duplicate mg\Profile for '{$profileName}'");
                }

                $profiles[$profileName] = $className;
                $mappings[$profileName] = $className;
            }

            $properties['profiles']= serialize($profiles);

        });

    }

    public function getProfileClass($profileName) {
        // Remove "\a" in "b\a"
        $profileName = mb_strrchr($profileName, '\\', true) . '\\';
        // Ensure "a\b\" format
        $profileName = trim(mb_strtolower($profileName), '\\') . '\\';

        if(isset($this->mappings[$profileName])) {
            return $this->mappings[$profileName];
        }

        return null;
    }

    public function getEnclosingProfiles($className) {
        $profileName = mb_strtolower($className);
        $profiles = array();

        do {
            	
            $profileName = mb_strrchr($profileName, '\\', true);

            if(isset($this->mappings[$profileName . '\\'])) {
                $profiles[] = $this->mappings[$profileName . '\\'];
            }

        } while($profileName !== false);

        return $profiles;
    }

    public function getEnclosedProfiles($className) {
        $result = array();

        $enclosingProfiles = $this->getEnclosingProfiles($className);

        if(count($enclosingProfiles) > 0) {
            $result[] = $enclosingProfiles[0];
        }

        $profiles = isset($this->properties['profiles']) ? unserialize($this->properties['profiles']) : array();

        $profileName = mb_strrchr(mb_strtolower($className), '\\', true) . '\\';

        foreach($profiles as $profile => $profileClass) {
            if(mb_strpos($profile, $profileName) === 0) {
                $result[] = $profileClass;
            }
        }

        return $result;
    }

    public function getProfiles() {
        $profiles = isset($this->properties['profiles']) ? unserialize($this->properties['profiles']) : array();

        return array_values($profiles);
    }
}

interface ApplicationMapper {

    public function getApplicationName($className);

    public function getApplicationClass($applicationName);

    public function getApplicationProfiles($applicationName);

    public function getApplications();

}

class XmlBasedApplicationMapper implements ApplicationMapper {

    /**
     * @var Storage
     */
    private $mappings = null;

    /**
     * @var Storage
     */
    private $properties = null;
    
    public function __construct($projectPath) {
        $storageManager = get('mg\StorageManager');
        $runtime = get('mg\Runtime');

        $mappings = $storageManager->getStorage('xmlbasedapplicationmapper.mappings', true);
        $this->mappings = $storageManager->cache($mappings);

        $properties = $storageManager->getStorage('xmlbasedapplicationmapper.properties', true);
        $this->properties = $storageManager->cache($properties);

        if($runtime->isTransactional()) {
            $this->rebuild($projectPath);
        }
    }
    

    private function rebuild($projectPath) {
        $mappings = $this->mappings;
        $properties = $this->properties;

        $realProjectPath = realpath($projectPath);

        if($realProjectPath === false) {
            throw new \mg\Exception("Project path '{$projectPath}' is not accessible");
        }

        $mappings->clear();
        $applications = isset($properties['applications']) ? unserialize($properties['applications']) : array();

        $iterator = new \RecursiveIteratorIterator(
            new RegexRecursiveDirectoryFilterIterator(
            new \RecursiveDirectoryIterator($realProjectPath, \RecursiveDirectoryIterator :: CURRENT_AS_FILEINFO)
                ,'#^[^.].*$#i'
                ,'#^[^.].*\.xml#i'));

        $architecture = get('mg\Architecture');

        $files = array();

        $profiles = array();

        /* Gather all files */
        foreach($iterator as $filename => $file) {
            if(!$file->isReadable()) {
                continue;
            }

            /* @var $xml \SimpleXmlElement */
            $element = simplexml_load_file($filename);
            $element->registerXpathNamespace('mg', 'http://code.google.com/p/mgframework/grove');

            /* Get all applications */
            $files[$filename] = $element->xpath('/mg:project/mg:applications/mg:application');

            /* Get all profiles */
            foreach($element->xpath('/mg:project/mg:profiles/mg:compositeProfile') as $compositeProfile) {
                $references = array();

                foreach($compositeProfile->xpath('./mg:profile') as $profileReference) {
                    $references[] = mb_strtolower(trim((string)$profileReference->attributes()->name));
                }

                $profiles[mb_strtolower(trim((string)$compositeProfile->attributes()->name))] = $references;
            }

            foreach($element->xpath('/mg:project/mg:profiles/mg:profile') as $profile) {
                $name = mb_strtolower((trim((string)$profile->attributes()->name)));
                $profiles[$name] = $name;
            }
        }

        foreach($files as $filename => $applicationElements) {
            foreach($applicationElements as $application) {
                $name = $application->attributes()->name;
                $name = mb_strtolower(trim($name));

                $class = $application->attributes()->class;
                $class = ltrim(trim($class), '\\');

                if(empty($name) && !empty($class)) {
                    throw new Exception("No name set for application with class '{$class}' in '{$filename}'");
                }

                if(empty($class) && !empty($name)) {
                    throw new Exception("No class set for application with name '{$name}' in '{$filename}'");
                }

                if(empty($class) && empty($name)) {
                    throw new Exception("Empty application in '{$filename}'");
                }
                
                if($architecture->getClass($class) === null) {
                    throw new Exception("Class '{$class}' for application '{$name}' in '{$filename}' does not exist");
                }

                if($architecture->getClassDistance($class, 'mg\Application') === false) {
                    throw new Exception("Class '{$class}' for application '{$name}' in '{$filename}' is not of type 'mg\Application'");
                }

                $fromClassKey = "cl:" . mb_strtolower($class);;
                $fromAppKey = "ap:{$name}";
                $profileKey = "pr:{$name}";
                
                if(isset($mappings[$fromAppKey])) {
                    throw new Exception("Duplicate application for name '{$name}' in '{$filename}'");
                }

                if(isset($mappings[$fromClassKey])) {
                    throw new Exception("Duplicate application for class '{$class}' in '{$filename}'");
                }

                $applicationProfiles = array();

                foreach($application->xpath('./mg:profile') as $applicationProfile) {
                    $profileName = mb_strtolower(trim($applicationProfile->attributes()->name));

                    if(!isset($profiles[$profileName])) {
                        throw new Exception("Unknown profile '{$profileName}' for application '{$name}' in '{$filename}'");
                    }

                    $applicationProfiles[] = $profiles[$profileName];
                }

                do {

                    $hasMoreArrays = false;

                    $newApplicationProfiles = array();
                    foreach($applicationProfiles as $key => $profile) {
                        if(is_array($profile)) {

                            foreach($profile as $pKey => $pValue) {
                                $profile[$pKey] = $profiles[$pValue];
                            }
                            
                            array_splice($newApplicationProfiles, 0, 0, $profile);

                            $hasMoreArrays = true;
                        } else {
                            $newApplicationProfiles[] = $profile;
                        }
                    }
                    
                    $applicationProfiles = $newApplicationProfiles;
                } while($hasMoreArrays);
                
                // gather profiles

                $applications[$name] = $class;

                $mappings[$fromAppKey] = mb_strtolower($class);
                $mappings[$fromClassKey] = $name;
                $mappings[$profileKey] = serialize($applicationProfiles);
            }
        }
        
        $properties['applications'] = serialize($applications);
    }
    
    public function getApplications() {
        if(!isset($this->properties['applications'])) {
            return array();
        }

        return array_keys(unserialize($this->properties['applications']));
    }

    public function getApplicationName($className) {
        $className = 'cl:'.mb_strtolower($className);

        if(isset($this->mappings[$className])) {
            return $this->mappings[$className];
        }

        return null;
    }

    public function getApplicationClass($applicationName) {
        $applicationName = 'ap:'.mb_strtolower($applicationName);

        if(isset($this->mappings[$applicationName])) {
            return $this->mappings[$applicationName];
        }

        return null;
    }

    public function getApplicationProfiles($applicationName) {
        $applicationName = 'pr:'.mb_strtolower($applicationName);

        if(isset($this->mappings[$applicationName])) {
            return unserialize($this->mappings[$applicationName]);
        }

        return null;
    }
}

class NamespaceBasedApplicationMapper implements ApplicationMapper {

    /**
     * @var Storage
     */
    private $mappings = null;

    /**
     * @var Storage
     */
    private $properties = null;

    public function __construct(StorageManager $storageManager, Runtime $runtime) {

        $mappings = $storageManager->getStorage('namespacebasedapplicationmapper.mappings', true);
        $this->mappings = $storageManager->cache($mappings);

        $properties = $storageManager->getStorage('namespacebasedapplicationmapper.properties', true);
        $this->properties = $storageManager->cache($properties);

        if($runtime->isTransactional()) {
            $this->rebuild();
        }
    }

    private function rebuild() {
        $mappings = $this->mappings;
        $properties = $this->properties;

        in(function(Architecture $architecture, ProfileMapper $profileMapper) use($mappings, $properties) {
            // find all applications
            // make sure them all be unique (by name)

            $applications = isset($properties['applications']) ? unserialize($properties['applications']) : array();
            $controllers = isset($properties['controllers']) ? unserialize($properties['controllers']) : array();

            $classUpdates = $architecture->getUpdates();

            // Cleanup removed applications / controllers etc

            /* @var $classUpdate ClassUpdate */
            foreach($classUpdates as $classUpdate) {
                $className = mb_strtolower($classUpdate->getQualifiedName());

                $applicationName = mb_strtolower($classUpdate->getNamespace());

                if(mb_strlen($applicationName) === 0) {
                    continue;
                }

                if(($pos = mb_strrpos($applicationName, '\\')) !== false) {
                    $applicationName = mb_substr($applicationName, $pos+1);
                }

                if(isset($applications[$applicationName]) && $applications[$applicationName] === $className) {
                    unset($applications[$applicationName]);
                    unset($mappings["ap:{$applicationName}"]);
                    unset($mappings["cl:{$className}"]);
                    unset($mappings["pr:{$applicationName}"]);
                }

                if(isset($controllers[$applicationName]) && $controllers[$applicationName] === $className) {
                    unset($controllers[$applicationName]);
                    unset($mappings["co:{$applicationName}"]);
                }
            }

            // Rebuild collection of applicationss
            	
            /* @var $classUpdate ClassUpdate */
            foreach($classUpdates as $classUpdate) {
                if($classUpdate->isRemoved()) {
                    continue;
                }

                $className = mb_strtolower($classUpdate->getQualifiedName());

                $applicationName = mb_strtolower($classUpdate->getNamespace());

                if(mb_strlen($applicationName) === 0) {
                    continue;
                }

                if(($pos = mb_strrpos($applicationName, '\\')) !== false) {
                    $applicationName = mb_substr($applicationName, $pos+1);
                }

                if($architecture->getClassDistance($className, 'mg\Application') > 0) {
                    if(isset($applications[$applicationName])) {
                        throw new Exception("Duplicate mg\Application for '{$applicationName}'");
                    }

                    $applications[$applicationName] = $className;

                    continue;
                }

                if($architecture->getClassDistance($className, 'mg\ApplicationController') > 0) {
                    if(isset($controllers[$applicationName])) {
                        throw new Exception("Duplicate mg\ApplicationController for '{$applicationName}'");
                    }

                    $controllers[$applicationName] = $className;

                    $mappings["co:{$applicationName}"] = $className;

                    continue;
                }

            }

            foreach($applications as $applicationName => $className) {
                $profiles = $profileMapper->getEnclosingProfiles($className);

                if(count($profiles) === 0) {
                    unset($mappings["ap:{$applicationName}"]);
                    unset($mappings["cl:{$className}"]);
                    unset($mappings["pr:{$applicationName}"]);

                    unset($applications[$applicationName]);

                    continue;
                }

                $mappings["ap:{$applicationName}"] = $className;
                $mappings["cl:{$className}"] = $applicationName;
                $mappings["pr:{$applicationName}"] = serialize($profiles);
            }

            $properties['applications'] = serialize($applications);
            $properties['controllers'] = serialize($controllers);

        });

    }

    public function getApplications() {
        if(!isset($this->properties['applications'])) {
            return array();
        }

        return array_keys(unserialize($this->properties['applications']));
    }

    public function getApplicationName($className) {
        $className = 'cl:'.mb_strtolower($className);

        if(isset($this->mappings[$className])) {
            return $this->mappings[$className];
        }

        return null;
    }

    public function getApplicationClass($applicationName) {
        $applicationName = 'ap:'.mb_strtolower($applicationName);

        if(isset($this->mappings[$applicationName])) {
            return $this->mappings[$applicationName];
        }

        return null;
    }

    public function getApplicationProfiles($applicationName) {
        $applicationName = 'pr:'.mb_strtolower($applicationName);

        if(isset($this->mappings[$applicationName])) {
            return unserialize($this->mappings[$applicationName]);
        }

        return null;
    }

}

interface ModuleMapper {

    /**
     * @param unknown_type $className
     * @return unknown_type
     */
    public function getModuleName($className);

    public function getModuleClass($applicationName, $moduleName);

    public function getModules();
}

class XmlBasedModuleMapper implements ModuleMapper {

    /**
     * @var Storage
     */
    private $mappings = null;

    /**
     * @var Storage
     */
    private $properties = null;
    
    public function __construct($projectPath) {
        $storageManager = get('mg\StorageManager');
        $runtime = get('mg\Runtime');

        $mappings = $storageManager->getStorage('xmlbasedmodulemapper.mappings', true);
        $this->mappings = $storageManager->cache($mappings);

        $properties = $storageManager->getStorage('xmlbasedmodulemapper.properties', true);
        $this->properties = $storageManager->cache($properties);

        if($runtime->isTransactional()) {
            $this->rebuild($projectPath);
        }
    }
    

    private function rebuild($projectPath) {
        $mappings = $this->mappings;
        $properties = $this->properties;

        $realProjectPath = realpath($projectPath);

        if($realProjectPath === false) {
            throw new \mg\Exception("Project path '{$projectPath}' is not accessible");
        }

        $mappings->clear();
        $modules = isset($properties['modules']) ? unserialize($properties['modules']) : array();

        $iterator = new \RecursiveIteratorIterator(
            new RegexRecursiveDirectoryFilterIterator(
            new \RecursiveDirectoryIterator($realProjectPath, \RecursiveDirectoryIterator :: CURRENT_AS_FILEINFO)
                ,'#^[^.].*$#i'
                ,'#^[^.].*\.xml#i'));

        $architecture = get('mg\Architecture');

        $files = array();

        $profiles = array();

        /* Gather all files */
        foreach($iterator as $filename => $file) {
            if(!$file->isReadable()) {
                continue;
            }

            /* @var $xml \SimpleXmlElement */
            $element = simplexml_load_file($filename);
            $element->registerXpathNamespace('mg', 'http://code.google.com/p/mgframework/grove');

            /* Get all modules */
            $files[$filename] = $element->xpath('/mg:project/mg:modules/mg:module');

            /* Get all profiles */
            foreach($element->xpath('/mg:project/mg:profiles/mg:compositeProfile') as $compositeProfile) {
                $references = array();

                foreach($compositeProfile->xpath('./mg:profile') as $profileReference) {
                    $references[] = mb_strtolower(trim((string)$profileReference->attributes()->name));
                }

                $profiles[mb_strtolower(trim((string)$compositeProfile->attributes()->name))] = $references;
            }

            foreach($element->xpath('/mg:project/mg:profiles/mg:profile') as $profile) {
                $name = mb_strtolower((trim((string)$profile->attributes()->name)));
                $profiles[$name] = $name;
            }
        }

        /* @var $applicationMapper \mg\ApplicationMapper */
        $applicationMapper = get('mg\ApplicationMapper');

        $applications = $applicationMapper->getApplications();

        /*
         * Inspect all files
         */
        foreach($files as $filename => $moduleElements) {
            /*
             * Inspect all module elements
             */
            foreach($moduleElements as $module) {
                $name = $module->attributes()->name;
                $name = mb_strtolower(trim($name));

                $class = $module->attributes()->class;
                $class = ltrim(trim($class), '\\');

                /*
                 * Basic sanity checks
                 */
                if(empty($name) && !empty($class)) {
                    throw new Exception("No name set for module with class '{$class}' in '{$filename}'");
                }

                if(empty($class) && !empty($name)) {
                    throw new Exception("No class set for module with name '{$name}' in '{$filename}'");
                }

                if(empty($class) && empty($name)) {
                    throw new Exception("Empty module in '{$filename}'");
                }
                
                if($architecture->getClass($class) === null) {
                    throw new Exception("Class '{$class}' for module '{$name}' in '{$filename}' does not exist");
                }

                if($architecture->getClassDistance($class, 'mg\Module') === false) {
                    throw new Exception("Class '{$class}' for module '{$name}' in '{$filename}' is not of type 'mg\Module'");
                }

                $fromClassKey = "cl:" . mb_strtolower($class);

                if(isset($mappings[$fromClassKey])) {
                    throw new Exception("Duplicate module for class '{$class}' in '{$filename}'");
                }

                /*
                 * Flatten profile definitions for module for later module-app matching
                 */
                $moduleProfiles = array();

                foreach($module->xpath('./mg:profile') as $moduleProfile) {
                    $profileName = mb_strtolower(trim($moduleProfile->attributes()->name));

                    if(!isset($profiles[$profileName])) {
                        throw new Exception("Unknown profile '{$profileName}' for module '{$name}' in '{$filename}'");
                    }

                    $moduleProfiles[] = $profiles[$profileName];
                }

                do {
                    $hasMoreArrays = false;

                    $newModuleProfiles = array();
                    foreach($moduleProfiles as $key => $profile) {
                        if(is_array($profile)) {

                            foreach($profile as $pKey => $pValue) {
                                $profile[$pKey] = $profiles[$pValue];
                            }
                            
                            array_splice($newModuleProfiles, 0, 0, $profile);

                            $hasMoreArrays = true;
                        } else {
                            $newModuleProfiles[] = $profile;
                        }
                    }

                    $moduleProfiles = $newModuleProfiles;
                } while($hasMoreArrays);
                
                // gather profiles

                $modules[$name] = $class;

                
                
                foreach($applications as $applicationName) {
                    $applicationProfiles = array_unique($applicationMapper->getApplicationProfiles($applicationName));

                    if(count(array_diff($moduleProfiles, $applicationProfiles)) === 0) {
                        $appModuleMapping = "mo:{$applicationName}.{$name}";
                        
                        if(isset($mappings[$appModuleMapping])) {
                            throw new Exception("Ambiguous definitions for module '{$name}' under application '{$applicationName}'");
                        }
                        $mappings[$appModuleMapping] = mb_strtolower(mb_strtolower($class));
                    }
                    
                }
                
                $mappings[$fromClassKey] = $name;
            }
        }
        
        $properties['modules'] = serialize($modules);

        // get profiles
        
        // collect all modules
            // for every module, get all profiles
                // flatten profile
                // for every app which offers all profiles, register module:map relation
    }

    /**
     * @param unknown_type $className
     * @return unknown_type
     */
    public function getModuleName($moduleClass) {
        $className = 'cl:'.mb_strtolower($moduleClass);

        if(isset($this->mappings[$className])) {
            return $this->mappings[$className];
        }

        return null;
    }

    public function getModuleClass($applicationName, $moduleName) {
        $moduleName = 'mo:' . mb_strtolower($applicationName) . '.' . mb_strtolower($moduleName);

        if(isset($this->mappings[$moduleName])) {
            return $this->mappings[$moduleName];
        }

        return null;
    }

    public function getModules() {
        $modules = isset($this->properties['modules']) ? unserialize($this->properties['modules']) : array();

        return array_keys($modules);
    }

}

class NamespaceBasedModuleMapper implements ModuleMapper {

    /**
     * @var Storage
     */
    private $mappings = null;

    /**
     * @var Storage
     */
    private $properties = null;

    public function __construct(StorageManager $storageManager, Runtime $runtime) {

        $mappings = $storageManager->getStorage('namespacebasedmodulemapper.mappings', true);
        $this->mappings = $storageManager->cache($mappings);

        $properties = $storageManager->getStorage('namespacebasedmodulemapper.properties', true);
        $this->properties = $storageManager->cache($properties);

        if($runtime->isTransactional()) {
            $this->rebuild();
        }
    }

    private function rebuild() {
        $mappings = $this->mappings;
        $properties = $this->properties;

        in(function(Architecture $architecture, ApplicationMapper $applicationMapper, ProfileMapper $profileMapper) use($mappings, $properties) {

            $modules = isset($properties['modules']) ? unserialize($properties['modules']) : array();
            $controllers = isset($properties['controllers']) ? unserialize($properties['controllers']) : array();
            $removeFromMap = isset($properties['removefrommap']) ? unserialize($properties['removefrommap']) : array();

            foreach($removeFromMap as $key => $_) {
                unset($mappings[$key]);
            }

            $classUpdates = $architecture->getUpdates();

            // Cleanup removed modules / controllers etc

            /* @var $classUpdate ClassUpdate */
            foreach($classUpdates as $classUpdate) {
                $className = mb_strtolower($classUpdate->getQualifiedName());

                $moduleName = mb_strtolower($classUpdate->getNamespace());

                if(mb_strlen($moduleName) === 0) {
                    continue;
                }

                if(($pos = mb_strrpos($moduleName, '\\')) !== false) {
                    $moduleName = mb_substr($moduleName, $pos+1);
                }

                if(isset($modules[$className]) && $modules[$className] === $moduleName) {
                    unset($modules[$className]);
                }

                if(isset($controllers[$className]) && $controllers[$className] === $moduleName) {
                    unset($controllers[$className]);
                }
            }

            // Rebuild collection of applicationss

            /* @var $classUpdate ClassUpdate */
            foreach($classUpdates as $classUpdate) {
                if($classUpdate->isRemoved()) {
                    continue;
                }

                $className = mb_strtolower($classUpdate->getQualifiedName());

                $moduleName = mb_strtolower($classUpdate->getNamespace());

                if(mb_strlen($moduleName) === 0) {
                    continue;
                }

                if(($pos = mb_strrpos($moduleName, '\\')) !== false) {
                    $moduleName = mb_substr($moduleName, $pos+1);
                }

                if($architecture->getClassDistance($className, 'mg\Module') > 0) {

                    $profiles = $profileMapper->getEnclosingProfiles($className);

                    if(count($profiles) === 0) {
                        continue;
                    }

                    $modules[$className] = $moduleName;

                    continue;
                }

                if($architecture->getClassDistance($className, 'mg\ModuleController') > 0) {
                    $controllers[$className] = $moduleName;

                    continue;
                }

            }


            	
            // recap all $modules with $profiles
            // foreach module, figure out the profiles
            // check whether there is a module.firstprofile collision
            // foreach module x application, figure out the most likely module

            $profileToModule = array();
            	
            foreach($modules as $className => $moduleName) {
                $profiles = $profileMapper->getEnclosingProfiles($className);

                if(count($profiles) === 0) {
                    continue;
                }

                $profile = $profiles[0];


                if(isset($profileToModule[$profile][$moduleName])) {
                    throw new Exception("Duplicate mg\Module '{$moduleName}' in mg\Profile '{$profile}'");
                }

                $profileToModule[$profile][$moduleName] = $className;
            }

            $applications = $applicationMapper->getApplications();

            $targetApps = array();

            foreach($applications as $application) {
                $applicationClass = $applicationMapper->getApplicationClass($application);
                $applicationProfiles = $profileMapper->getEnclosingProfiles($applicationClass);

                foreach($modules as $className => $moduleName) {
                    $moduleProfiles = $profileMapper->getEnclosedProfiles($className);

                    if(count($moduleProfiles) === 0) {
                        continue;
                    }

                    $moduleProfile = mb_strrchr($moduleProfiles[0], '\\', true) . '\\';

                    foreach($applicationProfiles as $applicationProfile) {
                        $applicationProfile = mb_strrchr($applicationProfile, '\\', true) . '\\';
                        $moduleProfile = mb_strrchr($moduleProfile, '\\', true) . '\\';

                        if($applicationProfile === $moduleProfile) {
                            if(isset($targetApps[$application][$moduleName])) {
                                $previousProfile = $targetApps[$application][$moduleName]['profile'];
                                if(mb_strlen($applicationProfile) > mb_strlen($previousProfile)) {
                                    $targetApps[$application][$moduleName] = array('class' => $className, 'profile'=>$applicationProfile);
                                }
                            } else {
                                $targetApps[$application][$moduleName] = array('class' => $className, 'profile'=>$applicationProfile);
                            }
                        }
                    }
                    	
                    //					$profileMapper
                    //
                    // if app.profile <= module.profile, app.module = moduleclass
                    // if isset app.module, battle modules with profile
                    	
                }

            }

            $removeFromMap = array();

            foreach($targetApps as $application => $applicationModules) {
                foreach($applicationModules as $module => $entry) {
                    $controller = null;
                    $profile = $entry['profile'];
                    	
                    reset($controllers);

                    while((list($controllerClass, $controllerModule) = each($controllers)) !== false && $controller === null) {

                        $controllerProfiles = $profileMapper->getEnclosedProfiles($controllerClass);


                        if($module === $controllerModule && count($controllerProfiles) !== 0) {
                            $controllerProfile = mb_strrchr($controllerProfiles[0], '\\', true) . '\\';

                            if($controllerProfile === $profile) {
                                $controller = $controllerClass;
                            }
                        }
                    }

                    if($controller !== null) {
                        $mappings["co:{$application}.{$module}"] = $controller;
                        $removeFromMap["co:{$application}.{$module}"] = $controller;
                    }

                    $mappings["mo:{$application}.{$module}"] = $entry['class'];
                    $removeFromMap["mo:{$application}.{$module}"] = $entry['class'];

                    $mappings["cl:{$entry['class']}"] = $module;
                    $removeFromMap["cl:{$entry['class']}"] = $module;
                }
            }
            // appname.modulename = moduleclass

            // foreach module : foreach app / find best app
            	
            $properties['removefrommap'] = serialize($removeFromMap);
            $properties['modules'] = serialize($modules);
            $properties['controllers'] = serialize($controllers);
        });


    }

    /**
     * @param unknown_type $className
     * @return unknown_type
     */
    public function getModuleName($moduleClass) {
        $className = 'cl:'.mb_strtolower($moduleClass);

        if(isset($this->mappings[$className])) {
            return $this->mappings[$className];
        }

        return null;
    }

    public function getModuleClass($applicationName, $moduleName) {
        $moduleName = 'mo:' . mb_strtolower($applicationName) . '.' . mb_strtolower($moduleName);

        if(isset($this->mappings[$moduleName])) {
            return $this->mappings[$moduleName];
        }

        return null;
    }

    public function getModules() {
        $modules = isset($this->properties['modules']) ? unserialize($this->properties['modules']) : array();

        return array_keys($modules);
    }
}
